package com.rosiminc.pm.game;

import java.util.ArrayList;
import java.util.Collections;

import com.rosiminc.pm.game.tiles.GameBoardTile;
import com.rosiminc.pm.game.tiles.GameTileFactory;
import com.rosiminc.pm.game.tiles.TileType;

/**
 * This class represents a PipesMania game instance. PipesMania is a game where
 * the user must make sure to close all open pipes by applying changes on their
 * state and connecting them with one another.
 * 
 * @author Shymon
 * 
 */
public class PipesManiaGame {

	private GameBoardTile[][] board;
	private Tile[][] solution;
	private int numRows;
	private int numCols;
	private PipesManiaOptions options;

	private int numCorrect;

	/**
	 * This constructor creates a new game with a specified size in terms of
	 * pipes.
	 * 
	 * @param numRows
	 *            The number of rows
	 * @param numCols
	 *            The number of columns
	 * @throws IllegalArgumentException
	 *             if number of rows is smaller than 1
	 * @throws IllegalArgumentException
	 *             if number of columns is smaller than 1
	 */
	public PipesManiaGame(int numRows, int numCols) {

		if (numRows < 1)
			throw new IllegalArgumentException(
					"Number of rows may not be smaller than 1");
		if (numCols < 1)
			throw new IllegalArgumentException(
					"Number of columns may not be smaller than 1");

		this.numRows = numRows;
		this.numCols = numCols;
		this.options = new PipesManiaOptions(numRows, numCols);
		this.numCols = 0;

		this.newGame();
	}

	/**
	 * This constructor creates a new game with specified options.
	 * 
	 * @param options
	 *            The game options
	 * @throws IllegalArgumentException
	 *             if null options are sent
	 */
	public PipesManiaGame(PipesManiaOptions options) {

		setGameOptions(options);

		this.newGame();
	}

	/**
	 * This method starts a new game.
	 */
	public void newGame() {
		this.numRows = options.getNumRows();
		this.numCols = options.getNumCols();
		this.initializeBoard();
		this.generateSolutionBoard();
		this.generateGameBoard();
		this.generateScore();
	}

	/**
	 * This constructor sets the game options.
	 * 
	 * @param options
	 *            The game options
	 * @throws IllegalArgumentException
	 *             if null options are sent
	 */
	public void setGameOptions(PipesManiaOptions options) {
		if (options == null)
			throw new IllegalArgumentException("Null options sent");

		this.options = options;
	}

	/**
	 * This method returns the game options.
	 * 
	 * @return the game options
	 */
	public PipesManiaOptions getGameOptions() {
		return options;
	}

	/**
	 * This method shuffles the game tiles.
	 */
	public void shuffleTiles() {
		for (int i = 0; i < numRows; i++)
			for (int j = 0; j < numCols; j++)
				board[i][j].shuffle();
	}

	/**
	 * This method determines if the game is ended.
	 * 
	 * @return If the game is won
	 */
	public boolean isWon() {
		return numCorrect == numRows * numCols;
	}

	/**
	 * This method returns the game board tile at a given position.
	 * 
	 * @param row
	 *            the row
	 * @param col
	 *            the column
	 * @return the game board tile
	 */
	public GameBoardTile getGameBoardTileAt(int row, int col) {
		validateRowColumn(row, col);
		return board[row][col];
	}

	/**
	 * This method asks a specified tile to perform its primary action.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 * @throws IllegalArgumentException
	 *             if row or column are out of usable area
	 */
	public void performActionA(int row, int col) {
		validateRowColumn(row, col);
		board[row][col].actionA();
		checkScoreChange(row, col);
	}

	/**
	 * This method asks a specified tile to perform its secondary action.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 * @throws IllegalArgumentException
	 *             if row or column are out of usable area
	 */
	public void performActionB(int row, int col) {
		validateRowColumn(row, col);
		board[row][col].actionB();
		checkScoreChange(row, col);
	}

	/**
	 * This method gets the tile located at the specified position.
	 * 
	 * @deprecated Use getGameBoardTileAt() instead
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 * @return The tile
	 * @throws IllegalArgumentException
	 *             if row or column is out of usable area
	 */
	public Tile getTileAt(int row, int col) {
		validateRowColumn(row, col);
		return board[row][col].getTile();
	}

	/**
	 * This method returns the type of the tile located at a specified position.
	 * 
	 * @deprecated Use getGameBoardTileAt() instead
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 * @return The tile's type
	 * @throws IllegalArgumentException
	 *             if row or column are out of usable area
	 */
	public TileType getTileTypeAt(int row, int col) {
		validateRowColumn(row, col);
		return board[row][col].getTileType();
	}

	/**
	 * This method gets the game board as a multidimensional array of tiles.
	 * 
	 * @deprecated Use getGameBoardTileAt() instead
	 * @return The game board
	 */
	public Tile[][] getBoard() {
		Tile[][] copy = new Tile[numRows][numCols];

		for (int row = 0; row < numRows; row++)
			for (int col = 0; col < numCols; col++)
				copy[row][col] = board[row][col].getTile();

		return copy;
	}

	/**
	 * This method returns the game board as it is.
	 * 
	 * @deprecated Use getGameBoardTileAt() instead
	 * @return The game board
	 */
	public GameBoardTile[][] getGameBoard() {
		GameBoardTile[][] copy = new GameBoardTile[numRows][numCols];

		for (int row = 0; row < numRows; row++)
			for (int col = 0; col < numCols; col++)
				copy[row][col] = board[row][col];

		return copy;
	}

	/**
	 * This method initialises the game board.
	 */
	private void initializeBoard() {
		board = new GameBoardTile[numRows][numCols];
		solution = new Tile[numRows][numCols];
	}

	/**
	 * This method creates the solution board.
	 */
	private void generateSolutionBoard() {
		this.setEvenValues();
		this.setOddValues();
	}

	/**
	 * This method sets the even values of the solution board randomly.
	 */
	private void setEvenValues() {
		for (int i = 0; i < numRows; i++)
			for (int j = 0; j < numCols; j++)
				if ((i + j) % 2 == 0) {
					setEvenValue(i, j);
				}
	}

	/**
	 * This method sets an individual even value for the solution board.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 */
	private void setEvenValue(int row, int col) {
		int num;
		boolean invalid;
		Tile temp;

		do {
			invalid = false;
			num = (int) (Math.random() * 15) + 1;
			temp = Tile.getTile(num);

			if (row == 0 && temp.isUp())
				invalid = true;
			else if (col == 0 && temp.isLeft())
				invalid = true;
			else if (row == numRows - 1 && temp.isDown())
				invalid = true;
			else if (col == numCols - 1 && temp.isRight())
				invalid = true;
		} while (invalid);

		solution[row][col] = temp;

	}

	/**
	 * This method sets the odd values of the solution board according to the
	 * generated even values.
	 */
	private void setOddValues() {
		for (int i = 0; i < numRows; i++)
			for (int j = 0; j < numCols; j++)
				if ((i + j) % 2 == 1) {
					setOddValue(i, j);
				}
	}

	/**
	 * This method sets an individual odd value for the solution board according
	 * to the surrounding generated even values.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 */
	private void setOddValue(int row, int col) {
		boolean left = (col == 0 ? false : solution[row][col - 1].isRight());
		boolean up = (row == 0 ? false : solution[row - 1][col].isDown());
		boolean right = (col == numCols - 1 ? false : solution[row][col + 1]
				.isLeft());
		boolean down = (row == numRows - 1 ? false : solution[row + 1][col]
				.isUp());

		Tile temp = Tile.getTile(left, up, right, down);
		solution[row][col] = temp;
	}

	/**
	 * This method generates the tile type board.
	 * 
	 * @return The tile type board
	 */
	private TileType[][] generateTileTypeBoard() {
		ArrayList<TileType> list = new ArrayList<TileType>();

		for (int i = 0; i < options.getNumFlip(); i++)
			list.add(TileType.FLIP);
		for (int i = 0; i < options.getNumBlock(); i++)
			list.add(TileType.BLOCK);
		for (int i = 0; i < options.getNumRotate(); i++)
			list.add(TileType.ROTATE);

		Collections.shuffle(list);

		TileType[][] typeBoard = new TileType[numRows][numCols];
		for (int row = 0; row < numRows; row++) {
			for (int col = 0; col < numCols; col++) {
				typeBoard[row][col] = list.get(row * numCols + col);
			}
		}

		return typeBoard;
	}

	/**
	 * This method generates the game board out of the generated solution. Tile
	 * types are used according to the game's options.
	 */
	private void generateGameBoard() {

		TileType[][] tileTypeBoard = generateTileTypeBoard();

		for (int row = 0; row < numRows; row++)
			for (int col = 0; col < numCols; col++) {
				board[row][col] = GameTileFactory.getTile(row, col,
						solution[row][col], tileTypeBoard[row][col]);
			}

		this.shuffleTiles();
	}

	/**
	 * This method sets the first score instance, hence the number of correctly
	 * placed tiles in the grid.
	 */
	private void generateScore() {
		numCorrect = 0;
		for (int r = 0; r < numRows; r++)
			for (int c = 0; c < numCols; c++) {
				checkTile(r, c);
			}
	}

	/**
	 * This method checks for the score of a specified tile and its
	 * surroundings.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 */
	public void checkScoreChange(int row, int col) {
		checkTile(row, col);
		if (row > 0)
			checkTile(row - 1, col);
		if (row < numRows - 1)
			checkTile(row + 1, col);
		if (col > 0)
			checkTile(row, col - 1);
		if (col < numCols - 1)
			checkTile(row, col + 1);
	}

	/**
	 * This method performs a check on a tile to determine if its state improved
	 * or not and applies the needed modification to the number of correctly
	 * placed tiles in the grid.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 */
	private void checkTile(int row, int col) {
		boolean wasSolved = board[row][col].getLastSolved();
		boolean isSolved = isTileSolved(row, col);

		board[row][col].setLastSolved(isSolved);

		if (wasSolved == isSolved)
			return;
		if (isSolved)
			numCorrect++;
		else
			numCorrect--;
	}

	/**
	 * This method checks if a given tile is correctly connected with
	 * surrounding pipes.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 * @return If the tile is properly connected
	 */
	private boolean isTileSolved(int row, int col) {
		Tile tile = board[row][col].getTile();

		if (tile.isUp())
			if (row == 0 || !board[row - 1][col].getTile().isDown())
				return false;

		if (tile.isDown())
			if (row == numRows - 1 || !board[row + 1][col].getTile().isUp())
				return false;

		if (tile.isRight())
			if (col == numCols - 1 || !board[row][col + 1].getTile().isLeft())
				return false;

		if (tile.isLeft())
			if (col == 0 || !board[row][col - 1].getTile().isRight())
				return false;

		return true;
	}

	/**
	 * This method validates the row and column entry, making sure both are
	 * inside usable grid space.
	 * 
	 * @param row
	 *            The tile's row
	 * @param col
	 *            The tile's column
	 * @return The tile
	 * @throws IllegalArgumentException
	 *             if row or column is out of usable area
	 */
	private void validateRowColumn(int row, int col) {
		if (row < 0 || row >= numRows)
			throw new IllegalArgumentException("Row outside of usable area");
		if (col < 0 || col >= numCols)
			throw new IllegalArgumentException("Column outside of usable area");
	}
}
